import fs from "fs-extra";
import path from "path";
import xlsx from 'xlsx';
import { JsonStructParser } from "./JsonStructParser.js";
import xml2js, { ElementCompact } from "xml-js";
import { IConvInfo } from "./BaseStructParser.js";
import { md5 } from "../tools/vendor.js";
import { CharLike, NumberLike, isComplexType } from "./def.js";
import { StructDef } from "../typings/index.js";
import _ from "lodash";
import { toolchain } from "../tools/toolchain.js";
import { SvnHolder } from "./SvnHolder.js";

export class Xls2Xml {

    private static ins: Xls2Xml;
    public static get Instance(): Xls2Xml {
        if(!Xls2Xml.ins) Xls2Xml.ins = new Xls2Xml();
        return Xls2Xml.ins;
    }

    private keyWordMap: Record<string, number> = {};

    public async execute(): Promise<void> {
        if (!toolchain.options.input) return;

        const tmpFolder = toolchain.options.publish ? toolchain.options.project + '_publish' : toolchain.options.project;
        const temp = path.join(toolchain.options.workspace!, tmpFolder);
        const xmlPath = path.join(temp, 'xml');
        const xlsPath = path.join(temp, 'xls');
        const structPath = path.join(temp, 'struct');
        
        // 更新svn
        await SvnHolder.Instance.checkSVN(!toolchain.options.debug, structPath, this.adjustSVN(toolchain.projCfg.Svn.DesignerSvn, toolchain.options.publish) + 'xml');
        await SvnHolder.Instance.checkSVN(!toolchain.options.debug, xmlPath, this.adjustSVN(toolchain.projCfg.Svn.DesignerSvn, toolchain.options.publish) + 'bin/Client');
        await SvnHolder.Instance.checkSVN(!toolchain.options.debug, xlsPath, this.adjustSVN(toolchain.projCfg.Svn.DesignerSvn, toolchain.options.publish) + 'xls');

        await JsonStructParser.Instance.readStructs(structPath, false);

        const macros = JsonStructParser.Instance.getMacrosOfFile('KeyWord.xml');
        for (const macro of macros) {
            this.keyWordMap[macro._attributes.cname] = Number(macro._attributes.value);
        }

        const xlsFiles = toolchain.options.input.split(',');
        for (const x of xlsFiles) {
            const xls = path.join(xlsPath, x);
            const cinfos = JsonStructParser.Instance.getConvInfosByXls(xls);

            if (cinfos.length == 0) {
                console.error('no convert info:', xls);
                continue;
            }

            const xlsxBook = xlsx.readFile(xls);
            for (const info of cinfos) {
                // 由于不支持服务器bin文件生成，故只处理前台表格
                if (!info.meta.endsWith('_Flash')) continue;
                const def = JsonStructParser.Instance.getStructDef(info.meta.replace('_Flash', 'M'));
                if (!def) {
                    console.error('no struct def:', info.meta);
                    continue;
                }

                const sheets = info.sheet.split(' ');
                const arr: any[] = [];
                for (const s of sheets) {
                    const sheet = xlsxBook.Sheets[s];
                    const rows = xlsx.utils.sheet_to_json(sheet, { blankrows: true });
                    for (const row of rows) {
                        if (this.isEmptyRow(row)) {
                            break;
                        }
                        const o = this.readRow(row, def);
                        arr.push(o);
                    }
                }

                if (def._attributes.sortkey) {
                    _.sortBy(arr, def._attributes.sortkey);
                }

                const doc = this.makeXmlDoc(info, arr);                
                await fs.writeFile(path.join(xmlPath, path.basename(info.bin.replace('.bin', '.xml'))), doc, 'utf-8');
            }
        }

        // 提交svn
        if(!toolchain.options.debug) {
            console.log("Committing...");
            await SvnHolder.Instance.commitSVN('xls2xml@fy-convertor~', xmlPath);
        } else {
            console.log("Debug mode, committing skipped.");
        }
    }

    private adjustSVN(raw: string, publish?: boolean) {
        if (publish) {
            raw = raw.replace('trunk', 'branches/publish')
        }
        return raw;
    }

    private readRow(row: any, def: StructDef, prefix = ''): any {
        const o: ElementCompact = {};
        if (def._attributes.rootflag) {
            o._attributes = { version: '1' };
        } else {
            o._attributes = { type: def._attributes.__rawName__ };
        }
        for (const entry of def.entries) {
            let arrSize = -1;
            if (entry._attributes.refer) {
                const referedEntry = def.entries.find(v => v._attributes.name == entry._attributes.refer)!;
                arrSize = Number(row[referedEntry._attributes.cname!]) || 0;
            } else if (entry._attributes.count) {
                if (entry._attributes.count.match(/^\d+$/)) {
                    arrSize = Number(entry._attributes.count);
                } else {
                    const macro = JsonStructParser.Instance.getMacro(entry._attributes.count);
                    if (!macro) {
                        console.error('no macro definition:', entry._attributes.count);
                        process.exit(1);
                    }
                    arrSize = Number(macro._attributes.value);
                }
            }

            if (isComplexType(entry._attributes.type)) {
                const entryDef = JsonStructParser.Instance.getStructDef(entry._attributes.type);
                if (!entryDef) {
                    console.error('no struct def:', entry._attributes.type);
                    process.exit(1);
                }
                if (arrSize >= 0) {
                    const arr = [];
                    for (let i = 0; i < arrSize; i++) {
                        const e = this.readRow(row, entryDef, entry._attributes.cname! + (i + 1));
                        arr.push(e);
                    }
                    o[entry._attributes.name] = arr;
                } else {
                    o[entry._attributes.name] = this.readRow(row, entryDef);
                }
            } else {
                if (arrSize >= 0) {
                    const arr = [];
                    for (let i = 0; i < arrSize; i++) {
                        const v = this.convertSimpleValue(row[entry._attributes.cname! + (i + 1)], entry._attributes.type);
                        arr.push(v);
                    }
                    o[entry._attributes.name] = { _text: arr.join('') };
                } else {
                    let v = this.convertSimpleValue(row[prefix + entry._attributes.cname], entry._attributes.type);
                    o[entry._attributes.name] = { _text: v };
                }
            }
        }
        return o;
    }

    private convertSimpleValue(v: number | string, type: string): number | string {
        if (v == undefined) {
            // xml里定了这个字段，但表格里没有对应的列
            v = NumberLike.includes(type) ? 0 : '';
        }
        if (NumberLike.includes(type)) {
            if (typeof(v) == 'string') {
                const kwv = this.keyWordMap[v];
                if (kwv != null) {
                    v = kwv;
                } else {
                    v = Number(v) || 0;
                }
            }
            if (typeof(v) == 'number') {
                if (CharLike.includes(type)) {
                    v = '0x' + v.toString(16) + ' ';
                } else {
                    v = String(v) + ' ';
                }
            }
        }
        return v;
    }

    private isEmptyRow(row: any): boolean {
        for (const key in row) {
            if (row[key]) {
                return false;
            }
        }
        return true;
    }

    private makeXmlDoc(info: IConvInfo, arr: ElementCompact[]): string {
        // var json = '{"name":{"_text":"Ali"},"age":{"_text":"30"}}';
        // var options = {compact: true};
        // var content = xml2js.json2xml(json, options);
        const o: ElementCompact = {};
        o[info.meta] = arr;
        const content = xml2js.js2xml(o, { compact: true, spaces: 4 });
        const hash = md5(content);

        const doc = `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<${info.meta}_Tab>
<TResHeadAll version="5">
    <resHead type="TResHead">
        <Magic>1397052237 </Magic>
        <Version>6 </Version>
        <Unit>0 </Unit>
        <Count>${arr.length} </Count>
        <MetalibHash>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</MetalibHash>
        <ResVersion>0 </ResVersion>
        <CreateTime>   0-00-00 00:00:00</CreateTime>
        <ResEncording>UTF-8</ResEncording>
        <ContentHash>${hash}</ContentHash>
    </resHead>
    <resHeadExt type="TResHeadExt">
        <DataOffset>136 </DataOffset>
    </resHeadExt>
</TResHeadAll>
${content}
</${info.meta}_Tab>`;

        return doc;
    }
}
